/*
 * ============================================================================
 *
 *  Zombie:Reloaded
 *
 *  File:          playerclasses.inc
 *  Type:          Core 
 *  Description:   Provides functions for managing classes.
 *
 *  Copyright (C) 2009-2010  Greyscale, Richard Helgeby
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ============================================================================
 */

/*
    Ideas for immunity modes for humans:
    - Zombies have to stab x times to infect.
    - Zombies have to hurt humans so they loose hp. When the hp reach zero (or
      below) they turn into zombies.
    - Fully imune to all damage. Can't take or give damage. Sould only be used
      on admin mode classes.
    
    TODO: Make class attributes for for changing model render mode and colors.
    
    TODO: Make class attributes for fancy effects, like a glow (if possible).
    
    TODO: Make immunity settings suitable for both teams, or certain zombie-
          only/human-only settings.
*/

#include "zr/playerclasses/classheaders.h"

/**
 * Empty filter structure.
 */
new ClassNoFilter[ClassFilter];

/**
 * Filter structure for excluding special classes.
 */
new ClassNoSpecialClasses[ClassFilter] = {false, 0, ZR_CLASS_SPECIALFLAGS, -1};

/**
 * The original class data. This array only changed when class data is loaded.
 * ZR_CLASS_CACHE_ORIGINAL is the cache type to this array.
 */
new ClassData[ZR_CLASS_MAX][ClassAttributes];

/**
 * The class thata cache that can be modified. ZR_CLASS_CACHE_MODIFIED is the
 * cache type to this array.
 */
new ClassDataCache[ZR_CLASS_MAX][ClassAttributes];

/**
 * Cache for player attributes. Makes it possible for one or more players to
 * have custom attributes. ZR_CLASS_CACHE_PLAYER is the cache type to this
 * array.
 */
new ClassPlayerCache[MAXPLAYERS + 1][ClassAttributes];

/**
 * Cache for storing global multipliers, per team and per attribute when
 * possible. Only attributes that support multipliers will be used, others are
 * ignored.
 */
new Float:ClassMultiplierCache[ZR_CLASS_TEAMCOUNT][ClassMultipliers];

/**
 * Number of classes loaded.
 */
new ClassCount;

/**
 * Specifies whether the class team requirements and attributes are valid or not.
 * Used to block events that happend before the module is done loading.
 */
new bool:ClassValidated;

/**
 * Stores what class the player has selected, for each team.
 */
new ClassSelected[MAXPLAYERS + 1][ZR_CLASS_TEAMCOUNT];

/**
 * Stores what class to be restored on next spawn, if available.
 */
new ClassSelectedNext[MAXPLAYERS + 1][ZR_CLASS_TEAMCOUNT];

/**
 * Cache for the currently selected team (admin menus).
 */
new ClassAdminTeamSelected[MAXPLAYERS + 1];

/**
 * Cookies for storing class indexes.
 */
new Handle:g_hClassCookieClassSelected[ZR_CLASS_TEAMCOUNT];

/**
 * Cache for the currently selected attribute multiplier (admin menus).
 */
new ClassMultipliers:ClassAdminAttributeSelected[MAXPLAYERS + 1];

/**
 * Specifies whether a player is currently in admin mode.
 */
new bool:ClassPlayerInAdminMode[MAXPLAYERS + 1];

/**
 * Specifies whether a player is allowed to change class with instant effect.
 * This is only used on human classes, and in combination with the
 * zr_classes_change_timelimit time limit, but could be used other places too.
 * The class menu will automatically check this setting and apply attributes if
 * set to true.
 */
new bool:ClassAllowInstantChange[MAXPLAYERS + 1];

/**
 * Global array to track the instant change time limit per client.
 */
new Handle:tClassInstantChange[MAXPLAYERS + 1];

/**
 * Cache for storing original model path before applying custom models. Used
 * when restoring to old model.
 */
new String:ClassOriginalPlayerModel[MAXPLAYERS + 1][PLATFORM_MAX_PATH];

/**
 * Specifies whether a player has spawned at least once, to check when a player
 * spawn for the first time.
 */
new bool:ClassPlayerSpawned[MAXPLAYERS + 1];

/**
 * Keeps track of if a client has been authorized as an admin.
 */
new bool:ClassAdminChecked[MAXPLAYERS + 1];

/**
 * Keeps track of if a client's cookies are loaded.
 */
new bool:ClassCookiesLoaded[MAXPLAYERS + 1];

/**
 * Keeps track of if all modules are done loading (configs executed).
 */
new bool:ClassModulesLoaded;

/**
 * Keeps track of if the ready event has been fired on a client.
 */
new bool:ClassReadyEventFired[MAXPLAYERS + 1];

/**
 * What method used to apply speed on players.
 */
new ClassSpeedMethods:ClassSpeedMethod = ClassSpeed_Prop;

#include "zr/playerclasses/filtertools"
#include "zr/playerclasses/attributes"
#include "zr/playerclasses/apply"
#include "zr/playerclasses/clientoverlays"
#include "zr/playerclasses/clientalpha"
#include "zr/playerclasses/healthregen"
#include "zr/playerclasses/classevents"
#include "zr/playerclasses/classmenus"
#include "zr/playerclasses/classcommands"

/**
 * Loads class attributes from the class file into ClassData array. If any
 * error occur the plugin load will fail, and errors will be logged.
 *
 * @param keepMultipliers   Optional. Don't reset multipliers. Default is
 *                          false.
 */
ClassLoad()
{
    // Register config file.
    ConfigRegisterConfig(File_Classes, Structure_Keyvalue, CONFIG_FILE_ALIAS_CLASSES);
    
    new Handle:kvClassData;
    
    // Make sure kvClassData is ready to use.
    kvClassData = CreateKeyValues(CONFIG_FILE_ALIAS_CLASSES);
    
    // Get weapons config path.
    decl String:pathclasses[PLATFORM_MAX_PATH];
    new bool:exists = ConfigGetCvarFilePath(CVAR_CONFIG_PATH_CLASSES, pathclasses);
    
    // If file doesn't exist, then log and stop.
    if (!exists)
    {
        LogEvent(false, LogTypeOld_Fatal, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Missing playerclasses config file \"%s\"", pathclasses);
        
        // Remove key/value cache.
        CloseHandle(kvClassData);
        kvClassData = INVALID_HANDLE;
        
        return;
    }
    
    // Log what class file that is loaded.
    LogEvent(false, LogTypeOld_Normal, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Loading classes from file \"%s\".", pathclasses);
    
    // Put file data into memory.
    FileToKeyValues(kvClassData, pathclasses);
    
    // Try to find the first class.
    KvRewind(kvClassData);
    if (!KvGotoFirstSubKey(kvClassData))
    {
        LogEvent(false, LogTypeOld_Fatal, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Can't find any classes in \"%s\"", pathclasses);
    }
    
    // Attribute string buffers.
    decl String:name[64];
    decl String:group[64];
    decl String:description[256];
    decl String:model_path[PLATFORM_MAX_PATH];
    decl String:overlay_path[PLATFORM_MAX_PATH];
    decl String:weapon_profile[32];
    
    // Quick string init.
    name[0] = 0;
    group[0] = 0;
    description[0] = 0;
    model_path[0] = 0;
    overlay_path[0] = 0;
    weapon_profile[0] = 0;
    
    ClassCount = 0;
    new failedcount;
    new ClassErrorFlags;
    
    // Loop through all classes and store attributes in the ClassData array.
    do
    {
        if (ClassCount > ZR_CLASS_MAX)
        {
            // Maximum classes reached. Write a warning and exit the loop.
            LogEvent(false, LogTypeOld_Error, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Warning: Maximum classes reached (%d). Skipping other classes.", ZR_CLASS_MAX + 1);
            
            break;
        }
        
        /* General */
        ClassData[ClassCount][Class_Enabled] = ConfigKvGetStringBool(kvClassData, "enabled", ZR_CLASS_DEFAULT_ENABLED);
        ClassData[ClassCount][Class_Team] = KvGetNum(kvClassData, "team", ZR_CLASS_DEFAULT_TEAM);
        ClassData[ClassCount][Class_TeamDefault] = ConfigKvGetStringBool(kvClassData, "team_default", ZR_CLASS_DEFAULT_TEAM_DEFAULT);
        ClassData[ClassCount][Class_Flags] = KvGetNum(kvClassData, "flags", ZR_CLASS_DEFAULT_FLAGS);
        
        KvGetString(kvClassData, "group", group, sizeof(group), ZR_CLASS_DEFAULT_GROUP);
        strcopy(ClassData[ClassCount][Class_Group], 64, group);
        
        KvGetString(kvClassData, "name", name, sizeof(name), ZR_CLASS_DEFAULT_NAME);
        strcopy(ClassData[ClassCount][Class_Name], 64, name);
        
        KvGetString(kvClassData, "description", description, sizeof(description), ZR_CLASS_DEFAULT_DESCRIPTION);
        strcopy(ClassData[ClassCount][Class_Description], 256, description);
        
        
        /* Model */
        KvGetString(kvClassData, "model_path", model_path, sizeof(model_path), ZR_CLASS_DEFAULT_MODEL_PATH);
        strcopy(ClassData[ClassCount][Class_ModelPath], PLATFORM_MAX_PATH, model_path);
        
        ClassData[ClassCount][Class_AlphaInitial] = KvGetNum(kvClassData, "alpha_initial", ZR_CLASS_DEFAULT_ALPHA_INITIAL);
        ClassData[ClassCount][Class_AlphaDamaged] = KvGetNum(kvClassData, "alpha_damaged", ZR_CLASS_DEFAULT_ALPHA_DAMAGED);
        ClassData[ClassCount][Class_AlphaDamage] = KvGetNum(kvClassData, "alpha_damage", ZR_CLASS_DEFAULT_ALPHA_DAMAGE);
        
        
        /* Hud */
        KvGetString(kvClassData, "overlay_path", overlay_path, sizeof(overlay_path), ZR_CLASS_DEFAULT_OVERLAY_PATH);
        strcopy(ClassData[ClassCount][Class_OverlayPath], PLATFORM_MAX_PATH, overlay_path);
        
        ClassData[ClassCount][Class_Nvgs] = ConfigKvGetStringBool(kvClassData, "nvgs", ZR_CLASS_DEFAULT_NVGS);
        ClassData[ClassCount][Class_Fov] = KvGetNum(kvClassData, "fov", ZR_CLASS_DEFAULT_FOV);
        
        
        /* Effects */
        ClassData[ClassCount][Class_HasNapalm] = ConfigKvGetStringBool(kvClassData, "has_napalm", ZR_CLASS_DEFAULT_HAS_NAPALM);
        ClassData[ClassCount][Class_NapalmTime] = KvGetFloat(kvClassData, "napalm_time", ZR_CLASS_DEFAULT_NAPALM_TIME);
        
        
        /* Player behaviour */
        ClassData[ClassCount][Class_ImmunityMode] = KvGetNum(kvClassData, "immunity_mode", ZR_CLASS_DEFAULT_IMMUNITY_MODE);
        ClassData[ClassCount][Class_ImmunityAmount] = KvGetFloat(kvClassData, "immunity_amount", ZR_CLASS_DEFAULT_IMMUNITY_AMOUNT);
        ClassData[ClassCount][Class_NoFallDamage] = ConfigKvGetStringBool(kvClassData, "no_fall_damage", ZR_CLASS_DEFAULT_NO_FALL_DAMAGE);
        
        KvGetString(kvClassData, "weapon_profile", weapon_profile, sizeof(weapon_profile), ZR_CLASS_DEFAULT_WEAPON_PROFILE);
        ClassData[ClassCount][Class_WeaponProfile] = WeaponProfilesGetIndex(weapon_profile);
        
        ClassData[ClassCount][Class_Health] = KvGetNum(kvClassData, "health", ZR_CLASS_DEFAULT_HEALTH);
        ClassData[ClassCount][Class_HealthRegenInterval] = KvGetFloat(kvClassData, "health_regen_interval", ZR_CLASS_DEFAULT_HEALTH_REGEN_INTERVAL);
        ClassData[ClassCount][Class_HealthRegenAmount] = KvGetNum(kvClassData, "health_regen_amount", ZR_CLASS_DEFAULT_HEALTH_REGEN_AMOUNT);
        ClassData[ClassCount][Class_HealthInfectGain] = KvGetNum(kvClassData, "health_infect_gain", ZR_CLASS_DEFAULT_HEALTH_INFECT_GAIN);
        ClassData[ClassCount][Class_KillBonus] = KvGetNum(kvClassData, "kill_bonus", ZR_CLASS_DEFAULT_KILL_BONUS);
        
        ClassData[ClassCount][Class_Speed] = KvGetFloat(kvClassData, "speed", ZR_CLASS_DEFAULT_SPEED);
        ClassData[ClassCount][Class_KnockBack] = KvGetFloat(kvClassData, "knockback", ZR_CLASS_DEFAULT_KNOCKBACK);
        ClassData[ClassCount][Class_JumpHeight] = KvGetFloat(kvClassData, "jump_height", ZR_CLASS_DEFAULT_JUMP_HEIGHT);
        ClassData[ClassCount][Class_JumpDistance] = KvGetFloat(kvClassData, "jump_distance", ZR_CLASS_DEFAULT_JUMP_DISTANCE);
        
        // Validate the class attributes.
        ClassErrorFlags = ClassValidateClassAttributes(ClassCount, true);
        if (ClassErrorFlags > 0)
        {
            // There's one or more invalid class attributes. Disable the class
            // and log an error message.
            ClassData[ClassCount][Class_Enabled] = false;
            LogEvent(false, LogTypeOld_Error, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Warning: Invalid class at index %d, disabled class. Class error flags: %d.", ClassCount, ClassErrorFlags);
            
            failedcount++;
        }
        
        // Update the counter.
        ClassCount++;
    } while (KvGotoNextKey(kvClassData));
    
    // Validate team requirements.
    if (!ClassValidateTeamRequirements())
    {
        LogEvent(false, LogTypeOld_Fatal, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "The class configuration doesn't match the team requirements.");
    }
    
    // Validate team default requirements.
    if (!ClassValidateTeamDefaults())
    {
        LogEvent(false, LogTypeOld_Fatal, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Couldn't find a default class for one or more teams. At least one class per team must be marked as default.");
    }
    
    // Cache class data.
    ClassReloadDataCache();
    
    // Reset selected class indexes for next spawn.
    ClassResetNextIndexes();
    
    // Mark classes as valid.
    ClassValidated = true;
    
    // Log summary.
    LogEvent(false, LogTypeOld_Normal, LOG_CORE_EVENTS, LogModule_Playerclasses, "Config Validation", "Total: %d | Successful: %d | Unsuccessful: %d", ClassCount, ClassCount - failedcount, failedcount);
    
    // Set config data.
    ConfigSetConfigLoaded(File_Classes, true);
    ConfigSetConfigReloadFunc(File_Classes, GetFunctionByName(GetMyHandle(), "ClassOnConfigReload"));
    ConfigSetConfigPath(File_Classes, pathclasses);
    
    // Remove key/value cache.
    CloseHandle(kvClassData);
    kvClassData = INVALID_HANDLE;
}

/**
 * Called when configs are being reloaded.
 * 
 * @param config    The config being reloaded.
 */
public ClassOnConfigReload(ConfigFile:config)
{
    // Reload class config.
    ClassLoad();
}

/** 
 * Gets the speed method.
 *
 * @return  Speed method, or ClassSpeed_Invalid on error.
 */
ClassSpeedMethods:ClassGetSpeedMethod()
{
    decl String:speedMethod[16];
    speedMethod[0] = 0;
    
    GetConVarString(g_hCvarsList[CVAR_CLASSES_SPEED_METHOD], speedMethod, sizeof(speedMethod));
    
    if (StrEqual(speedMethod, "lmv", false))
    {
        return ClassSpeed_LMV;
    }
    else if (StrEqual(speedMethod, "prop", false))
    {
        return ClassSpeed_Prop;
    }
    /* Not implemented yet.
    else if (StrEqual(speedMethod, "command", false))
    {
        return ClassSpeed_LMV;
    }*/
    
    return ClassSpeed_Invalid;
}

/**
 * Updates the class data cache. Original values are retrieved from ClassData.
 *
 * @return      True on success, false otherwise.
 */
bool:ClassReloadDataCache()
{
    // Check if there are no classes.
    if (ClassCount == 0)
    {
        return false;
    }
    
    // Loop through all classes.
    for (new classindex = 0; classindex < ClassCount; classindex++)
    {
        /* General */
        ClassDataCache[classindex][Class_Enabled] = ClassData[classindex][Class_Enabled];
        ClassDataCache[classindex][Class_Team] = ClassData[classindex][Class_Team];
        ClassDataCache[classindex][Class_TeamDefault] = ClassData[classindex][Class_TeamDefault];
        ClassDataCache[classindex][Class_Flags] = ClassData[classindex][Class_Flags];
        strcopy(ClassDataCache[classindex][Class_Group], 64, ClassData[classindex][Class_Group]);
        strcopy(ClassDataCache[classindex][Class_Name], 64, ClassData[classindex][Class_Name]);
        strcopy(ClassDataCache[classindex][Class_Description], 256, ClassData[classindex][Class_Description]);
        
        /* Model */
        strcopy(ClassDataCache[classindex][Class_ModelPath], PLATFORM_MAX_PATH, ClassData[classindex][Class_ModelPath]);
        ClassDataCache[classindex][Class_AlphaInitial] = ClassData[classindex][Class_AlphaInitial];
        ClassDataCache[classindex][Class_AlphaDamaged] = ClassData[classindex][Class_AlphaDamaged];
        ClassDataCache[classindex][Class_AlphaDamage] = ClassData[classindex][Class_AlphaDamage];
        
        /* Hud */
        strcopy(ClassDataCache[classindex][Class_OverlayPath], PLATFORM_MAX_PATH, ClassData[classindex][Class_OverlayPath]);
        ClassDataCache[classindex][Class_Nvgs] = ClassData[classindex][Class_Nvgs];
        ClassDataCache[classindex][Class_Fov] = ClassData[classindex][Class_Fov];
        
        /* Effects */
        ClassDataCache[classindex][Class_HasNapalm] = ClassData[classindex][Class_HasNapalm];
        ClassDataCache[classindex][Class_NapalmTime] = ClassData[classindex][Class_NapalmTime];
        
        /* Player behaviour */
        ClassDataCache[classindex][Class_ImmunityMode] = ClassData[classindex][Class_ImmunityMode];
        ClassDataCache[classindex][Class_ImmunityAmount] = ClassData[classindex][Class_ImmunityAmount];
        ClassDataCache[classindex][Class_NoFallDamage] = ClassData[classindex][Class_NoFallDamage];
        ClassDataCache[classindex][Class_WeaponProfile] = ClassData[classindex][Class_WeaponProfile];
        ClassDataCache[classindex][Class_Health] = ClassData[classindex][Class_Health];
        ClassDataCache[classindex][Class_HealthRegenInterval] = ClassData[classindex][Class_HealthRegenInterval];
        ClassDataCache[classindex][Class_HealthRegenAmount] = ClassData[classindex][Class_HealthRegenAmount];
        ClassDataCache[classindex][Class_HealthInfectGain] = ClassData[classindex][Class_HealthInfectGain];
        ClassDataCache[classindex][Class_KillBonus] = ClassData[classindex][Class_KillBonus];
        ClassDataCache[classindex][Class_Speed] = ClassData[classindex][Class_Speed];
        ClassDataCache[classindex][Class_KnockBack] = ClassData[classindex][Class_KnockBack];
        ClassDataCache[classindex][Class_JumpHeight] = ClassData[classindex][Class_JumpHeight];
        ClassDataCache[classindex][Class_JumpDistance] = ClassData[classindex][Class_JumpDistance];
    }
    
    return true;
}

/**
 * Refresh the specified player's cache from the specified class data cache.
 *
 * @param client        The client index.
 * @param classindex    The index of the class to read from.
 * @param cachetype     Optional. Specifies what class cache to read from.
 *                      Options: ZR_CLASS_CACHE_ORIGINAL (unchanged class
 *                      data), ZR_CLASS_CACHE_MODIFIED (default, modified class
 *                      data).
 * @return      True if successful, false otherwise.
 */
bool:ClassReloadPlayerCache(client, classindex, cachetype = ZR_CLASS_CACHE_MODIFIED)
{
    // Validate indexes.
    if (!ClassValidateIndex(classindex) || !ZRIsClientValid(client))
    {
        return false;
    }
    
    switch (cachetype)
    {
        case ZR_CLASS_CACHE_ORIGINAL:
        {
            /* General */
            ClassPlayerCache[client][Class_Enabled] = ClassData[classindex][Class_Enabled];
            ClassPlayerCache[client][Class_Team] = ClassData[classindex][Class_Team];
            ClassPlayerCache[client][Class_TeamDefault] = ClassData[classindex][Class_TeamDefault];
            ClassPlayerCache[client][Class_Flags] = ClassData[classindex][Class_Flags];
            strcopy(ClassPlayerCache[client][Class_Group], 64, ClassData[classindex][Class_Group]);
            strcopy(ClassPlayerCache[client][Class_Name], 64, ClassData[classindex][Class_Name]);
            strcopy(ClassPlayerCache[client][Class_Description], 256, ClassData[classindex][Class_Description]);
            
            /* Model */
            strcopy(ClassPlayerCache[client][Class_ModelPath], PLATFORM_MAX_PATH, ClassData[classindex][Class_ModelPath]);
            ClassPlayerCache[client][Class_AlphaInitial] = ClassData[classindex][Class_AlphaInitial];
            ClassPlayerCache[client][Class_AlphaDamaged] = ClassData[classindex][Class_AlphaDamaged];
            ClassPlayerCache[client][Class_AlphaDamage] = ClassData[classindex][Class_AlphaDamage];
            
            /* Hud */
            strcopy(ClassPlayerCache[client][Class_OverlayPath], PLATFORM_MAX_PATH, ClassData[classindex][Class_OverlayPath]);
            ClassPlayerCache[client][Class_Nvgs] = ClassData[classindex][Class_Nvgs];
            ClassPlayerCache[client][Class_Fov] = ClassData[classindex][Class_Fov];
            
            /* Effects */
            ClassPlayerCache[client][Class_HasNapalm] = ClassData[classindex][Class_HasNapalm];
            ClassPlayerCache[client][Class_NapalmTime] = ClassData[classindex][Class_NapalmTime];
            
            /* Player behaviour */
            ClassPlayerCache[client][Class_ImmunityMode] = ClassData[classindex][Class_ImmunityMode];
            ClassPlayerCache[client][Class_ImmunityAmount] = ClassData[classindex][Class_ImmunityAmount];
            ClassPlayerCache[client][Class_NoFallDamage] = ClassData[classindex][Class_NoFallDamage];
            ClassPlayerCache[client][Class_WeaponProfile] = ClassData[classindex][Class_WeaponProfile];
            ClassPlayerCache[client][Class_Health] = ClassData[classindex][Class_Health];
            ClassPlayerCache[client][Class_HealthRegenInterval] = ClassData[classindex][Class_HealthRegenInterval];
            ClassPlayerCache[client][Class_HealthRegenAmount] = ClassData[classindex][Class_HealthRegenAmount];
            ClassPlayerCache[client][Class_HealthInfectGain] = ClassData[classindex][Class_HealthInfectGain];
            ClassPlayerCache[client][Class_KillBonus] = ClassData[classindex][Class_KillBonus];
            ClassPlayerCache[client][Class_Speed] = ClassData[classindex][Class_Speed];
            ClassPlayerCache[client][Class_KnockBack] = ClassData[classindex][Class_KnockBack];
            ClassPlayerCache[client][Class_JumpHeight] = ClassData[classindex][Class_JumpHeight];
            ClassPlayerCache[client][Class_JumpDistance] = ClassData[classindex][Class_JumpDistance];
        }
        case ZR_CLASS_CACHE_MODIFIED:
        {
            /* General */
            ClassPlayerCache[client][Class_Enabled] = ClassDataCache[classindex][Class_Enabled];
            ClassPlayerCache[client][Class_Team] = ClassDataCache[classindex][Class_Team];
            ClassPlayerCache[client][Class_TeamDefault] = ClassDataCache[classindex][Class_TeamDefault];
            ClassPlayerCache[client][Class_Flags] = ClassDataCache[classindex][Class_Flags];
            strcopy(ClassPlayerCache[client][Class_Group], 64, ClassDataCache[classindex][Class_Group]);
            strcopy(ClassPlayerCache[client][Class_Name], 64, ClassDataCache[classindex][Class_Name]);
            strcopy(ClassPlayerCache[client][Class_Description], 256, ClassDataCache[classindex][Class_Description]);
            
            /* Model */
            strcopy(ClassPlayerCache[client][Class_ModelPath], PLATFORM_MAX_PATH, ClassDataCache[classindex][Class_ModelPath]);
            ClassPlayerCache[client][Class_AlphaInitial] = ClassDataCache[classindex][Class_AlphaInitial];
            ClassPlayerCache[client][Class_AlphaDamaged] = ClassDataCache[classindex][Class_AlphaDamaged];
            ClassPlayerCache[client][Class_AlphaDamage] = ClassDataCache[classindex][Class_AlphaDamage];
            
            /* Hud */
            strcopy(ClassPlayerCache[client][Class_OverlayPath], PLATFORM_MAX_PATH, ClassDataCache[classindex][Class_OverlayPath]);
            ClassPlayerCache[client][Class_Nvgs] = ClassDataCache[classindex][Class_Nvgs];
            ClassPlayerCache[client][Class_Fov] = ClassDataCache[classindex][Class_Fov];
            
            /* Effects */
            ClassPlayerCache[client][Class_HasNapalm] = ClassDataCache[classindex][Class_HasNapalm];
            ClassPlayerCache[client][Class_NapalmTime] = ClassDataCache[classindex][Class_NapalmTime];
            
            /* Player behaviour */
            ClassPlayerCache[client][Class_ImmunityMode] = ClassDataCache[classindex][Class_ImmunityMode];
            ClassPlayerCache[client][Class_ImmunityAmount] = ClassDataCache[classindex][Class_ImmunityAmount];
            ClassPlayerCache[client][Class_NoFallDamage] = ClassDataCache[classindex][Class_NoFallDamage];
            ClassPlayerCache[client][Class_WeaponProfile] = ClassDataCache[classindex][Class_WeaponProfile];
            ClassPlayerCache[client][Class_Health] = ClassDataCache[classindex][Class_Health];
            ClassPlayerCache[client][Class_HealthRegenInterval] = ClassDataCache[classindex][Class_HealthRegenInterval];
            ClassPlayerCache[client][Class_HealthRegenAmount] = ClassDataCache[classindex][Class_HealthRegenAmount];
            ClassPlayerCache[client][Class_HealthInfectGain] = ClassDataCache[classindex][Class_HealthInfectGain];
            ClassPlayerCache[client][Class_KillBonus] = ClassDataCache[classindex][Class_KillBonus];
            ClassPlayerCache[client][Class_Speed] = ClassDataCache[classindex][Class_Speed];
            ClassPlayerCache[client][Class_KnockBack] = ClassDataCache[classindex][Class_KnockBack];
            ClassPlayerCache[client][Class_JumpHeight] = ClassDataCache[classindex][Class_JumpHeight];
            ClassPlayerCache[client][Class_JumpDistance] = ClassDataCache[classindex][Class_JumpDistance];
        }
        default:
        {
            // Invalid cache specified.
            return false;
        }
    }
    
    return true;
}

/**
 * Refresh the specified player's cache and re-apply attributes.
 *
 * @param client        The client index.
 * @return              True if successful, false otherwise.
 */
bool:ClassReloadPlayer(client)
{
    new activeclass;
    
    // Get active class index.
    activeclass = ClassGetActiveIndex(client);
    
    // Validate index.
    if (activeclass < 0)
    {
        return false;
    }
    
    // Refresh cache and re-apply attributes.
    ClassOnClientDeath(client);         // Dummy event to clean up and turn off stuff.
    ClassReloadPlayerCache(client, activeclass);
    ClassApplyAttributes(client);
    
    return true;
}

/**
 * Reset all class attribute multipliers to 1.0.
 */
ClassResetMultiplierCache()
{
    // Loop through all teams.
    for (new teamid = 0; teamid < ZR_CLASS_TEAMCOUNT; teamid++)
    {
        ClassMultiplierCache[teamid][ClassM_NapalmTime] = 1.0;
        ClassMultiplierCache[teamid][ClassM_Health] = 1.0;
        ClassMultiplierCache[teamid][ClassM_HealthRegenInterval] = 1.0;
        ClassMultiplierCache[teamid][ClassM_HealthRegenAmount] = 1.0;
        ClassMultiplierCache[teamid][ClassM_HealthInfectGain] = 1.0;
        ClassMultiplierCache[teamid][ClassM_Speed] = 1.0;
        ClassMultiplierCache[teamid][ClassM_Knockback] = 1.0;
        ClassMultiplierCache[teamid][ClassM_JumpHeight] = 1.0;
        ClassMultiplierCache[teamid][ClassM_JumpDistance] = 1.0;
    }
}

/**
 * Resets the selected class indexes for next spawn on one or all clients.
 *
 * @param client    Optional. Specify client to reset. Default is all.
 */
ClassResetNextIndexes(client = -1)
{
    new teamid;
    
    if (client > 0)
    {
        for (teamid = 0; teamid < ZR_CLASS_TEAMCOUNT; teamid++)
        {
            ClassSelectedNext[client][teamid] = -1;
        }
    }
    else
    {
        for (client = 1; client <= MAXPLAYERS; client++)
        {
            for (teamid = 0; teamid < ZR_CLASS_TEAMCOUNT; teamid++)
            {
                ClassSelectedNext[client][teamid] = -1;
            }
        }
    }
}

/**
 * Restores next class indexes on a player, if available.
 * Note: Does not apply attributes. The classes are only marked as selected.
 *
 * @param client        The client index.
 * @param excludeTeam   Optional. Do not restore the specified team.
 */
ClassRestoreNextIndexes(client, excludeTeam = -1)
{
    // Get next class indexes.
    new zombie = ClassSelectedNext[client][ZR_CLASS_TEAM_ZOMBIES];
    new human = ClassSelectedNext[client][ZR_CLASS_TEAM_HUMANS];
    new admin = ClassSelectedNext[client][ZR_CLASS_TEAM_ADMINS];
    
    // Check if the zombie team should be excluded.
    if (excludeTeam != ZR_CLASS_TEAM_ZOMBIES)
    {
        // Validate zombie class index.
        if (ClassValidateIndex(zombie))
        {
            // Mark next zombie class as selected.
            ClassSelected[client][ZR_CLASS_TEAM_ZOMBIES] = zombie;
        }
        
        // Reset index.
        ClassSelectedNext[client][ZR_CLASS_TEAM_ZOMBIES] = -1;
    }
    
    // Check if the human team should be excluded.
    if (excludeTeam != ZR_CLASS_TEAM_HUMANS)
    {
        // Validate human class index.
        if (ClassValidateIndex(human))
        {
            // Mark next zombie class as selected.
            ClassSelected[client][ZR_CLASS_TEAM_HUMANS] = human;
        }
        
        // Reset index.
        ClassSelectedNext[client][ZR_CLASS_TEAM_HUMANS] = -1;
    }
    
    // Check if the human team should be excluded.
    if (excludeTeam != ZR_CLASS_TEAM_ADMINS)
    {
        // Validate admin class index.
        if (ClassValidateIndex(admin))
        {
            // Mark next zombie class as selected.
            ClassSelected[client][ZR_CLASS_TEAM_ADMINS] = admin;
        }
        
        // Reset index.
        ClassSelectedNext[client][ZR_CLASS_TEAM_ADMINS] = -1;
    }
}

/**
 * Reset cached states for the client ready event.
 *
 * @param client    Client index.
 */
ClassResetReadyState(client)
{
    ClassAdminChecked[client] = false;
    ClassCookiesLoaded[client] = false;
    ClassReadyEventFired[client] = false;
}

/**
 * Returns whether the client is ready or not.
 *
 * @param client    Client index.
 * @return          True if ready, false otherwise.
 */
bool:ClassIsClientReady(client)
{
    if (ClassModulesLoaded &&
        IsClientConnected(client) &&
        ClassAdminChecked[client] &&
        ClassCookiesLoaded[client])
    {
        return true;
    }
    
    return false;
}

/**
 * Fires the ready event if the client is ready.
 *
 * Note: This function is called in the following events in classevents.inc:
 *       ClassOnClientConnected
 *       ClassOnPostAdminCheck
 *       ClassOnCookiesCached
 *
 * @param client    Client index.
 */
ClassCheckReadyState(client)
{
    if (ClassIsClientReady(client))
    {
        // Fire ready event.
        if (!ClassReadyEventFired[client])
        {
            ClassReadyEventFired[client] = true;
            ClassOnClientReady(client);
        }
    }
}

/**
 * Fires the ready event for all clients that is ready.
 *
 * Note: This function is called in ClassOnModulesLoaded.
 */
ClassCheckReadyStates()
{
    for (new client = 1; client < MaxClients; client++)
    {
        ClassCheckReadyState(client);
    }
}

/**
 * Sets default class indexes for each team on all players, or a single player
 * if specified.
 *
 * @param client    Optional. The client index. If specified, cookies are used.
 * @param loadMode  Optional. How class selections should be applied on the
 *                  client. Default is ClassLoadMode_Normal.
 */
ClassClientSetDefaultIndexes(client = -1, ClassLoadMode:loadMode = ClassLoadMode_Normal)
{
    new bool:clientvalid = ZRIsClientValid(client);
    new filter[ClassFilter];
    new bool:saveclasses = GetConVarBool(g_hCvarsList[CVAR_CLASSES_SAVE]);
    
    new zombieindex;
    new humanindex;
    new adminindex;
    
    new bool:haszombie;
    new bool:hashuman;
    new bool:hasadmin;
    
    
    /*
     *  SETUP CLASS FILTER
     */
    
    // Do not require any class flags to be set.
    filter[ClassFilter_RequireFlags] = 0;
    
    // Set filter to hide mother zombie classes.
    filter[ClassFilter_DenyFlags] = ZR_CLASS_FLAG_MOTHER_ZOMBIE;
    
    // Set filter to also hide admin-only classes if not admin.
    filter[ClassFilter_DenyFlags] += !ZRIsClientAdmin(client) ? ZR_CLASS_FLAG_ADMIN_ONLY : 0;
    
    // Specify client so it can check group permissions.
    filter[ClassFilter_Client] = client;
    
    
    /*
     *  GET CLASS INDEXES
     */
    
    // Check if a client is specified.
    if (clientvalid && !(loadMode == ClassLoadMode_Preload))
    {
        // Get cookie indexes if enabled.
        if (saveclasses)
        {
            zombieindex = CookiesGetInt(client, g_hClassCookieClassSelected[ZR_CLASS_TEAM_ZOMBIES]);
            humanindex = CookiesGetInt(client, g_hClassCookieClassSelected[ZR_CLASS_TEAM_HUMANS]);
            adminindex = CookiesGetInt(client, g_hClassCookieClassSelected[ZR_CLASS_TEAM_ADMINS]);
        }
        else
        {
            // Do not use indexes in cookies. Set invalid values so it will
            // fall back to default class.
            zombieindex = 0;
            humanindex = 0;
            adminindex = 0;
        }
        
        // Note: When class indexes are set on cookies, they're incremented by
        //       one so zero means no class set and will result in a invalid
        //       class index when restored. The indexes are shifted because
        //       new players will have blank cookies (0), so we define 0 as a
        //       invalid index.
        
        // Check if class indexes are set and that the client pass the filter.
        // Also check that the saved class' team id match with the loaded class.
        // If not, fall back to default class indexes. Otherwise substract
        // index by one.
        if (zombieindex <= 0 ||
            !ClassTeamCompare(zombieindex - 1, ZR_CLASS_TEAM_ZOMBIES) ||
            !ClassFilterMatch(zombieindex - 1, filter))
        {
            zombieindex = ClassGetDefaultSpawnClass(ZR_CLASS_TEAM_ZOMBIES, filter);
        }
        else
        {
            zombieindex--;
            haszombie = true;
        }
        
        if (humanindex <= 0 ||
            !ClassTeamCompare(humanindex - 1, ZR_CLASS_TEAM_HUMANS) ||
            !ClassFilterMatch(humanindex - 1, filter))
        {
            humanindex = ClassGetDefaultSpawnClass(ZR_CLASS_TEAM_HUMANS, filter);
        }
        else
        {
            humanindex--;
            hashuman = true;
        }
        
        if (adminindex <= 0 ||
            !ClassTeamCompare(adminindex - 1, ZR_CLASS_TEAM_ADMINS) ||
            !ClassFilterMatch(adminindex - 1, filter))
        {
            adminindex = ClassGetDefaultSpawnClass(ZR_CLASS_TEAM_ADMINS, filter);
        }
        else
        {
            adminindex--;
            hasadmin = true;
        }
    }
    else
    {
        // Set filter to exclude classes that require groups.
        filter[ClassFilter_Client] = -1;
        
        // Get default class indexes.
        zombieindex = ClassGetDefaultSpawnClass(ZR_CLASS_TEAM_ZOMBIES, filter);
        humanindex = ClassGetDefaultSpawnClass(ZR_CLASS_TEAM_HUMANS, filter);
        adminindex = ClassGetDefaultSpawnClass(ZR_CLASS_TEAM_ADMINS, filter);
    }
    
    
    /*
     *  VALIDATE INDEXES
     */
    if (!ClassValidateIndex(zombieindex))
    {
        // Invalid class index. Fall back to default class in class config and
        // log a warning.
        LogEvent(false, LogTypeOld_Error, LOG_CORE_EVENTS, LogModule_Playerclasses, "Set Default Indexes", "Warning: Failed to get the specified zombie class, falling back to default class in class config. Check spelling in \"zr_classes_default_zombie\".");
        
        // Use default class.
        zombieindex = ClassGetDefaultClass(ZR_CLASS_TEAM_ZOMBIES, filter);
    }
    
    if (!ClassValidateIndex(humanindex))
    {
        // Invalid class index. Fall back to default class in class config and
        // log a warning.
        LogEvent(false, LogTypeOld_Error, LOG_CORE_EVENTS, LogModule_Playerclasses, "Set Default Indexes", "Warning: Failed to get the specified human class, falling back to default class in class config. Check spelling in \"zr_classes_default_human\".");
        
        // Use default class.
        humanindex = ClassGetDefaultClass(ZR_CLASS_TEAM_HUMANS, filter);
    }
    
    if (!ClassValidateIndex(adminindex))
    {
        // Invalid class index. Fall back to default class in class config if
        // possible. A invalid class index (-1) can also be stored if there are
        // no admin classes at all.
        adminindex = ClassGetDefaultClass(ZR_CLASS_TEAM_ADMINS, filter);
    }
    
    
    /*
     *  MARK INDEXES AS SELECTED, UPDATE CACHE AND COOKIES
     */
    
    // Check if a client is specified.
    if (clientvalid)
    {
        // Set selected class idexes, or if post loading set classes to change
        // time if client already spawned (too late to change directly then).
        if (loadMode == ClassLoadMode_Postload && IsPlayerAlive(client))
        {
            // Get the class that the client is using.
            new activeclass = ClassGetActiveIndex(client);
            
            // Set classes, except for the active one.
            if (ClassTeamCompare(activeclass, ZR_CLASS_TEAM_ZOMBIES))
            {
                // Set all classes, except for the zombie class.
                ClassSelectedNext[client][ZR_CLASS_TEAM_ZOMBIES] = zombieindex;
                ClassSelected[client][ZR_CLASS_TEAM_HUMANS] = humanindex;
                ClassSelected[client][ZR_CLASS_TEAM_ADMINS] = adminindex;
            }
            else if (ClassTeamCompare(activeclass, ZR_CLASS_TEAM_HUMANS))
            {
                // Set all classes, except for the human class.
                ClassSelected[client][ZR_CLASS_TEAM_ZOMBIES] = zombieindex;
                ClassSelectedNext[client][ZR_CLASS_TEAM_HUMANS] = humanindex;
                ClassSelected[client][ZR_CLASS_TEAM_ADMINS] = adminindex;
            }
            else
            {
                // Set all classes, except for the admin class.
                ClassSelected[client][ZR_CLASS_TEAM_ZOMBIES] = zombieindex;
                ClassSelected[client][ZR_CLASS_TEAM_HUMANS] = humanindex;
                ClassSelectedNext[client][ZR_CLASS_TEAM_ADMINS] = adminindex;
            }
        }
        else
        {
            ClassSelected[client][ZR_CLASS_TEAM_ZOMBIES] = zombieindex;
            ClassSelected[client][ZR_CLASS_TEAM_HUMANS] = humanindex;
            ClassSelected[client][ZR_CLASS_TEAM_ADMINS] = adminindex;
            
            // Copy human class data to player cache.
            ClassReloadPlayerCache(client, humanindex);
        }
        
        // Save indexes in cookies if enabled, and not already saved.
        if (saveclasses && !(loadMode == ClassLoadMode_Preload))
        {
            if (!haszombie)
            {
                CookiesSetInt(client, g_hClassCookieClassSelected[ZR_CLASS_TEAM_ZOMBIES], zombieindex + 1);
            }
            if (!hashuman)
            {
                CookiesSetInt(client, g_hClassCookieClassSelected[ZR_CLASS_TEAM_HUMANS], humanindex + 1);
            }
            if (!hasadmin)
            {
                CookiesSetInt(client, g_hClassCookieClassSelected[ZR_CLASS_TEAM_ADMINS], adminindex + 1);
            }
        }
    }
    else
    {
        // No client specified. Loop through all players.
        for (new clientindex = 1; clientindex <= MaxClients; clientindex++)
        {
            // Set selected class idexes.
            ClassSelected[clientindex][ZR_CLASS_TEAM_ZOMBIES] = zombieindex;
            ClassSelected[clientindex][ZR_CLASS_TEAM_HUMANS] = humanindex;
            ClassSelected[clientindex][ZR_CLASS_TEAM_ADMINS] = adminindex;
            
            // Copy human class data to player cache.
            ClassReloadPlayerCache(client, humanindex);
        }
    }
}

/**
 * Dump class data into a string. String buffer length should be at about 2048
 * cells.
 *
 * @param index     Index of the class in a class cache or a client index,
 *                  depending on the cache type specified.
 * @param cachetype Optional. Specifies what class cache to read from. Options:
 *                  ZR_CLASS_CACHE_ORIGINAL - Unchanced class data.
 *                  ZR_CLASS_CACHE_MODIFIED (default) - Changed/newest class
 *                  data.
 *                  ZR_CLASS_CACHE_PLAYER - Player cache. If this one is used,
 *                  index will be used as a client index.
 * @return          Number of cells written.
 */
ClassDumpData(index, cachetype, String:buffer[], maxlen)
{
    new cellcount;
    decl String:attribute[320];
    decl String:format_buffer[256];
    
    if (maxlen == 0)
    {
        return 0;
    }
    
    Format(format_buffer, sizeof(format_buffer), "Class data at index %d:\n", index);
    cellcount += StrCat(buffer, maxlen, format_buffer);
    cellcount += StrCat(buffer, maxlen, "-------------------------------------------------------------------------------\n");
    
    Format(attribute, sizeof(attribute), "enabled:               \"%d\"\n", ClassIsEnabled(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "team:                  \"%d\"\n", ClassGetTeamID(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "team_default:          \"%d\"\n", ClassGetTeamDefault(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "flags:                 \"%d\"\n", ClassGetFlags(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    ClassGetGroup(index, format_buffer, sizeof(format_buffer), cachetype);
    Format(attribute, sizeof(attribute), "group:                 \"%s\"\n", format_buffer);
    cellcount += StrCat(buffer, maxlen, attribute);
    
    ClassGetName(index, format_buffer, sizeof(format_buffer), cachetype);
    Format(attribute, sizeof(attribute), "name:                  \"%s\"\n", format_buffer);
    cellcount += StrCat(buffer, maxlen, attribute);
    
    ClassGetDescription(index, format_buffer, sizeof(format_buffer), cachetype);
    Format(attribute, sizeof(attribute), "description:           \"%s\"\n", format_buffer);
    cellcount += StrCat(buffer, maxlen, attribute);
    
    ClassGetModelPath(index, format_buffer, sizeof(format_buffer), cachetype);
    Format(attribute, sizeof(attribute), "model_path:            \"%s\"\n", format_buffer);
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "alpha_initial:         \"%d\"\n", ClassGetAlphaInitial(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "alpha_damaged:         \"%d\"\n", ClassGetAlphaDamaged(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "alpha_damage:          \"%d\"\n", ClassGetAlphaDamage(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    ClassGetOverlayPath(index, format_buffer, sizeof(format_buffer), cachetype);
    Format(attribute, sizeof(attribute), "overlay_path:          \"%s\"\n", format_buffer);
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "nvgs:                  \"%d\"\n", ClassGetNvgs(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "fov:                   \"%d\"\n", ClassGetFOV(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "has_napalm:            \"%d\"\n", ClassGetHasNapalm(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "napalm_time:           \"%0.2f\"\n", ClassGetNapalmTime(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "immunity_mode:         \"%d\"\n", ClassGetImmunityMode(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "immunity_amount:       \"%0.2f\"\n", ClassGetImmunityAmount(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "no_fall_damage:        \"%d\"\n", ClassGetNoFallDamage(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    WeaponProfilesGetName(ClassGetWeaponProfile(index, cachetype), format_buffer, sizeof(format_buffer));
    Format(attribute, sizeof(attribute), "weapon_profile:        \"%s\"\n", format_buffer);
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "health:                \"%d\"\n", ClassGetHealth(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "health_regen_interval: \"%0.2f\"\n", ClassGetHealthRegenInterval(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "health_regen_amount:   \"%d\"\n", ClassGetHealthRegenAmount(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "health_infect_gain:    \"%d\"\n", ClassGetHealthInfectGain(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "kill_bonus:            \"%d\"\n", ClassGetKillBonus(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "speed:                 \"%0.2f\"\n", ClassGetSpeed(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "knockback:             \"%0.2f\"\n", ClassGetKnockback(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "jump_height:           \"%0.2f\"\n", ClassGetJumpHeight(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    Format(attribute, sizeof(attribute), "jump_distance:         \"%0.2f\"\n", ClassGetJumpDistance(index, cachetype));
    cellcount += StrCat(buffer, maxlen, attribute);
    
    return cellcount;
}

/**
 * Converts a multiplier attribute to a human readable string.
 *
 * @param client        The client index to translate correct language. 
 * @param attribute     Attribute to convert.
 * @param buffer        Destination string buffer.
 * @param maxlen        Size of buffer.
 * @return              Number of cells written.
 */
ClassMultiplierToString(client, ClassMultipliers:attribute, String:buffer[], maxlen)
{
    decl String:phrase[48];
    
    SetGlobalTransTarget(client);
    switch (attribute)
    {
        case ClassM_NapalmTime:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Napalm Time");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_Health:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Health");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_HealthRegenInterval:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Regen Interval");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_HealthRegenAmount:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Regen Amount");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_HealthInfectGain:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Infect Gain");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_Speed:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Speed");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_Knockback:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Knockback");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_JumpHeight:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Jump Height");
            return strcopy(buffer, maxlen, phrase);
        }
        case ClassM_JumpDistance:
        {
            Format(phrase, sizeof(phrase), "%t", "Classes Attrib Jump Distance");
            return strcopy(buffer, maxlen, phrase);
        }
    }
    
    return 0;
}
